package cm

import (
	"context"
	"errors"
	"sync"

	"law-api-master/cm/client"
	"law-api-master/conf"
	"law-api-master/log"

	"github.com/PicoChain/accounts/abi"
	"github.com/PicoChain/accounts/abi/bind"
	"github.com/PicoChain/common"
	"github.com/PicoChain/crypto"
)

//ClientManager  manager severl clients and contract handlers. When we get status of picoChain,or
//transact contract message to picoChain, it will distribute all to availdable nodes.
type ClientManager struct {
	//user status cache
	userNonces map[common.Address]uint64
	userMux    sync.Mutex

	//key
	keyAcc  *bind.TransactOpts
	keyLock sync.Mutex

	//chargeUser
	chargeAcc  *bind.TransactOpts
	chargeLock sync.Mutex

	//upload
	uploadLock sync.Mutex

	//node
	clients map[string]*client.Client

	//proof contract
	contracters []proofContract
	abiData     abi.ABI
}

//NewClientManager according to the urls, create serverl clients to connect picochain node.
func NewClientManager(config *conf.ConfigParams) *ClientManager {
	cm := &ClientManager{
		keyAcc:      bind.NewKeyedTransactor(config.Chain.PoolKey()),
		chargeAcc:   bind.NewKeyedTransactor(config.Chain.ChargeAccKey()),
		userNonces:  make(map[common.Address]uint64),
		clients:     make(map[string]*client.Client),
		contracters: make([]proofContract, 0),
	}

	//register client to connect picochain nodes
	cm.registerClient(config.Chain.NodesURL())
	if len(cm.clients) == 0 {
		panic("can't find nodes")
	}

	//register contracter to connect proof contract
	cm.registerContract(config.Contracts)
	if len(cm.contracters) == 0 {
		panic("can't find contract")
	}

	//refresh current nonce
	nonce, err := cm.GetUserNonce(cm.keyAcc.From)
	if err != nil {
		panic("init nonce error")
	}
	log.Info("poolAcc loading succeed", "Addr", cm.keyAcc.From.Hex(),
		"Nonce", nonce, "CallClient", len(cm.clients),
		"CallContract", len(cm.contracters))

	return cm
}

//registerClient find several online picoChain nodes.
func (cm *ClientManager) registerClient(nodes []string) {
	for _, url := range nodes {
		c, err := client.Dial(url)
		if err != nil {
			log.Warn("Dial failed", "url", url, "err", err)
			continue
		}
		cm.clients[url] = c
	}
}

//registerContract add all contract we supported to call
func (cm *ClientManager) registerContract(configs []*conf.ContractParams) {
	//find all client to call contract
	cs := make([]*client.Client, 0)
	for _, c := range cm.clients {
		cs = append(cs, c)
	}

	//register supporting contract
	for _, contParams := range configs {
		switch contParams.ContractVersion() {
		case contV1:
			cont := newProofFir(contParams, cs)
			cm.contracters = append(cm.contracters, cont)
			log.Info("register contract", "addr", cont.addr.Hex(), "height", cont.blockHeight())

		case contV2:
			cont := newProofSec(contParams, cs, cm.getReceiptByTxHash)
			cm.contracters = append(cm.contracters, cont)
			log.Info("register contract", "addr", cont.addr.Hex(), "height", cont.blockHeight())

		default:
			log.Warn("unknow contract")
		}

	}

	//TODO: check contracter order

}

//randomContracter choose one client connected picochain
func (cm *ClientManager) randomClient() (string, *client.Client) {
	for k, c := range cm.clients {
		if c != nil {
			return k, c
		}
	}

	return "", nil
}

//SetUserNonce set the number of transaction have been committed in a account.
func (cm *ClientManager) SetUserNonce(addr common.Address, nonce uint64) {
	cm.userMux.Lock()
	defer cm.userMux.Unlock()
	cm.userNonces[addr] = nonce
}

//GetUserNonce get the number of transaction have been committed in a account.
func (cm *ClientManager) GetUserNonce(addr common.Address) (uint64, error) {
	cm.userMux.Lock()
	defer cm.userMux.Unlock()
	//loading cache status
	if nonce, ok := cm.userNonces[addr]; ok {
		return nonce, nil
	}

	//rpc request status, storage it
	_, c := cm.randomClient()
	nonce, err := c.PendingNonceAt(context.TODO(), addr)
	if err != nil {
		return uint64(0), err
	}
	cm.userNonces[addr] = nonce

	return nonce, nil
}

//ClearUserNonce clear the number of historical transation in a account.
func (cm *ClientManager) ClearUserNonce(hexkey string) {
	cm.userMux.Lock()
	defer cm.userMux.Unlock()

	// transfer privkey to address in picoChain
	key, _ := crypto.HexToECDSA(hexkey)
	addr := crypto.PubkeyToAddress(key.PublicKey)

	//clear its nonce cache
	delete(cm.userNonces, addr)
}

//curContracter return the latest contract,and we use it to storage proof data
func (cm *ClientManager) curContracter() proofContract {
	if len(cm.contracters) < 1 {
		return nil
	}
	return cm.contracters[len(cm.contracters)-1]
}

//findContract find contract from last. When the height transaction standing is
// higher than latest contract deployed, return this contract
func (cm *ClientManager) findContract(height uint64) (proofContract, error) {
	for i := len(cm.contracters) - 1; i >= 0; i-- {
		if height > cm.contracters[i].blockHeight() {
			return cm.contracters[i], nil
		}
	}
	return nil, errors.New("invalid contracter")
}
